;;; tail.el --- Tail files within Emacs

;; Copyright (C) 2000 by Benjamin Drieu

;; Author: Benjamin Drieu <bdrieu@april.org>
;; Keywords: tools

;; This file is NOT part of GNU Emacs.

;; This program as GNU Emacs are free software; you can redistribute
;; them and/or modify them under the terms of the GNU General Public
;; License as published by the Free Software Foundation; either
;; version 2, or (at your option) any later version.

;; They are distributed in the hope that they will be useful, but
;; WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
;; General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with them; see the file COPYING.  If not, write to the Free
;; Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
;; 02111-1307, USA.

;;  $Id: tail.el,v 1.2 2010/10/24 16:23:11 benj Exp $

;;; Commentary:

;;  This program displays ``tailed'' contents of files inside
;;  transients windows of Emacs.  It is primarily meant to keep an eye
;;  on logs within Emacs instead of using additional terminals.

;;  This was developed for GNU Emacs 20.x but should work as well for
;;  XEmacs 21.x


;;; Code:

;;  Custom variables (may be set by the user)

(defgroup tail nil
  "Tail files or commands into Emacs buffers."
  :prefix "tail-"
  :group 'environment)

(defcustom tail-volatile t 
  "Use non-nil to erase previous output"
  :options '(nil t)
  :group 'tail)

(defcustom tail-audible nil 
  "Use non-nil to produce a bell when some output is displayed"
  :options '(nil t)
  :group 'tail)

(defcustom tail-raise nil 
  "Use non-nil to raise current frame when some output is displayed (could be *very* annoying)"
  :options '(nil t)
  :group 'tail)

(defcustom tail-hide-delay 5 
  "Time in seconds before a tail window is deleted"
  :type 'integer
  :group 'tail)

(defcustom tail-max-size 5 
  "Maximum size of the window"
  :type 'integer
  :group 'tail)


;; Functions

;; Taken from calendar/appt.el
(defun tail-disp-window (tail-buffer tail-msg)
  "Display some content specified by ``tail-msg'' inside buffer
``tail-msg''.  Create this buffer if necessary and put it inside a
newly created window on the lowest side of the frame."

  (require 'electric)

  ;; Make sure we're not in the minibuffer
  ;; before splitting the window.

  (if (equal (selected-window) (minibuffer-window))
      (if (other-window 1) 
	    (select-window (other-window 1))
	(if (and window-system (other-frame 1))
	        (select-frame (other-frame 1)))))
      
  (let* ((this-buffer (current-buffer))
	  (this-window (selected-window))
	   (tail-disp-buf (set-buffer (get-buffer-create tail-buffer))))

    (if (cdr (assq 'unsplittable (frame-parameters)))
	;; In an unsplittable frame, use something somewhere else.
	(display-buffer tail-disp-buf)
      (unless (or (special-display-p (buffer-name tail-disp-buf))
		    (same-window-p (buffer-name tail-disp-buf))
		      (get-buffer-window tail-buffer))
	;; By default, split the bottom window and use the lower part.
	(tail-select-lowest-window)
	(split-window))
      (pop-to-buffer tail-disp-buf))

    (toggle-read-only 0)
    (if tail-volatile
	(erase-buffer))
    (insert-string tail-msg)
    (toggle-read-only 1)
    (shrink-window-if-larger-than-buffer (get-buffer-window tail-disp-buf t))
    (if (> (window-height (get-buffer-window tail-disp-buf t)) tail-max-size)
	(shrink-window (- (window-height (get-buffer-window tail-disp-buf t)) tail-max-size)))
    (set-buffer-modified-p nil)
    (if tail-raise
	(raise-frame (selected-frame)))
    (select-window this-window)
    (if tail-audible
	(beep 1))
    (if tail-hide-delay
	(run-with-timer tail-hide-delay nil 'tail-hide-window tail-buffer))))


(defun tail-hide-window (buffer)   
  (delete-window (get-buffer-window buffer t))); TODO: cancel timer when some output comes during that time


(defun tail-select-lowest-window ()
  "Select the lowest window on the frame."
  (let* ((lowest-window (selected-window))
	  (bottom-edge (car (cdr (cdr (cdr (window-edges))))))
         (last-window (previous-window))
         (window-search t))
    (while window-search
      (let* ((this-window (next-window))
	          (next-bottom-edge (car (cdr (cdr (cdr 
						           (window-edges this-window)))))))
	(if (< bottom-edge next-bottom-edge)
	        (progn
		        (setq bottom-edge next-bottom-edge)
			      (setq lowest-window this-window)))

	(select-window this-window)
	(if (eq last-window this-window)
	        (progn
		        (select-window lowest-window)
			      (setq window-search nil)))))))


(defun tail-file (file)
  "Tails file specified with argument ``file'' inside a new buffer.
``file'' *cannot* be a remote file specified with ange-ftp syntaxm
because it is passed to the Unix tail command."
  (interactive "Ftail file: ")
  (tail-command "tail" "-F" file)) ; TODO: what if file is remote (i.e. via ange-ftp)
  

(defun tail-command (command &rest args)
  "Tails command specified with argument ``command'', with arguments
``args'' inside a new buffer.  It is also called by tail-file"
  (interactive "sTail command: \neToto: ")
  (let ((process 
	  (apply 'start-process-shell-command
		 command 
		 (concat "*Tail: " 
			 command 
			 (if args " " "")
			 (mapconcat 'identity args " ") 
			 "*")
		 command 
		 args)))     
    (set-process-filter process 'tail-filter)))
  

(defun tail-filter (process line)
  "Tail filter called when some output comes."
  (tail-disp-window (process-buffer process) line))


(provide 'tail)

;;; tail.el ends here